Анализ пользователей мобильного приложения¶

Цель проекта

Исследование проводится с целью улучшения показателей мобильного приложения. Поставлена задача анализа маркетинговой воронки для выделения узких мест и формулирования предложений. С целью принятия бизнес-решения об изменении шрифта будет проведен анализ А/B-тестирования

Исходные данные

Исходные данные в одном файле формата .csv

План работ¶

  • Чтение и первичный анализ массива данных, импорт библиотек
  • Предобработка данных
    • Названия полей
    • Типы данных
    • Пропуски
    • Неявные/явные дубликаты
    • Работа с датой
  • Исследовательский анализ данных
    • Общая статистика
    • Анализ границ данных по дате
    • Контроль групп тестирования
  • Событийная аналитика
    • Распределение событий
    • Порядок событий
    • Построение воронки событий
    • Определение узких мест
    • Конверсия в покупателя
  • Анализ результатов эксперимента
    • Размеры групп
    • Статистически значимое различие контрольных групп (А/А)
    • Статистически значимое различие количества каждого события по контрольным группам
    • Статистически значимое различие количества каждого события по всем группам
    • Статистически значимое различие количества каждого события по всем группам, контрольная группа объединена
    • Анализ принимаемого уровня статистической значимости
  • Общий вывод

Чтение данных и импорт библиотек¶

In [1]:
# pandas
import pandas as pd
pd.options.mode.chained_assignment = None  # default='warn'
pd.options.display.float_format = '{:,.2f}'.format

# visualization
import matplotlib.pyplot as plt
import seaborn as sns
from plotly import graph_objects as go

# statistics
from scipy import stats as st
from statsmodels.stats.proportion import proportions_ztest

# other
import numpy as np
from datetime import datetime
In [2]:
# reading from local file
df = pd.read_csv('/Users/ilatti/Documents/practicum/10_sprint/logs_exp.csv', sep="\t")
In [3]:
print(df.info())
df.head()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 244126 entries, 0 to 244125
Data columns (total 4 columns):
 #   Column          Non-Null Count   Dtype 
---  ------          --------------   ----- 
 0   EventName       244126 non-null  object
 1   DeviceIDHash    244126 non-null  int64 
 2   EventTimestamp  244126 non-null  int64 
 3   ExpId           244126 non-null  int64 
dtypes: int64(3), object(1)
memory usage: 7.5+ MB
None
Out[3]:
EventName DeviceIDHash EventTimestamp ExpId
0 MainScreenAppear 4575588528974610257 1564029816 246
1 MainScreenAppear 7416695313311560658 1564053102 246
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248
3 CartScreenAppear 3518123091307005509 1564054127 248
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248
  • Всего 244 тыс. строк
  • Пропусков нет
  • Дата в формате Unix time

Предобработка данных¶

Названия столбцов¶

Назовем столбцы однообразно, в змеином регистре:

In [4]:
df.columns = df.columns.str.lower()
df.columns
Out[4]:
Index(['eventname', 'deviceidhash', 'eventtimestamp', 'expid'], dtype='object')
In [5]:
df = df.rename(columns={'eventname':'event_name',
                        'deviceidhash':'device_id_hash',
                        'eventtimestamp':'event_timestamp',
                        'expid':'exp_id'})
df.head()
Out[5]:
event_name device_id_hash event_timestamp exp_id
0 MainScreenAppear 4575588528974610257 1564029816 246
1 MainScreenAppear 7416695313311560658 1564053102 246
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248
3 CartScreenAppear 3518123091307005509 1564054127 248
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248

Преобразование типов данных¶

Все типы данных соответствуют содержимому (кроме даты, ее поправим позже)

Пропуски в данных¶

Пропусков не обнаружено

Дубликаты¶

Проведем проверку на неявные дубликаты в названиях событий

In [6]:
df.event_name.unique()
Out[6]:
array(['MainScreenAppear', 'PaymentScreenSuccessful', 'CartScreenAppear',
       'OffersScreenAppear', 'Tutorial'], dtype=object)

Неявных дубликатов не обнаружено; проведем проверку на явные дубликаты

In [7]:
print(f'Строк-дубликатов {df.duplicated().sum() / df.shape[0]:.1%} от общего количества')
Строк-дубликатов 0.2% от общего количества

Есть небольшое количество полных дубликатов, удалим их:

In [8]:
df = df.drop_duplicates()
df.duplicated().sum()
Out[8]:
0

Работа с датой¶

Добавим отдельные столбцы с датой в формате datetime

In [9]:
df['event_dt'] = pd.to_datetime(df['event_timestamp'], unit='s')
df['event_date'] = df['event_dt'].dt.date
df.head()
Out[9]:
event_name device_id_hash event_timestamp exp_id event_dt event_date
0 MainScreenAppear 4575588528974610257 1564029816 246 2019-07-25 04:43:36 2019-07-25
1 MainScreenAppear 7416695313311560658 1564053102 246 2019-07-25 11:11:42 2019-07-25
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248 2019-07-25 11:28:47 2019-07-25
3 CartScreenAppear 3518123091307005509 1564054127 248 2019-07-25 11:28:47 2019-07-25
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248 2019-07-25 11:48:42 2019-07-25

Вывод¶

  • Для удобства работы изменены названия столбцов
  • Пропусков в данных не обнаружено
  • Удалены полные дубликаты, удаленные данные составили 0.2 % всех строк
  • Добавлено два столбца с датой в формате datetime

Исследовательский анализ данных¶

Временной интервал выгрузки¶

In [10]:
print(f'Минимальная дата: {df.event_dt.min()}')
print(f'Максимальная дата: {df.event_dt.max()}')
Минимальная дата: 2019-07-25 04:43:36
Максимальная дата: 2019-08-07 21:15:17

Визуализируем плотность данных по датам

In [11]:
plt.figure(figsize=(16, 5))
df.event_dt.hist(bins=100, grid=False);
plt.title('Распределение количества действий по датам' + "\n", fontsize = 18)
plt.ylabel('Количество действий');
  • На графике можно заметить стабильные суточные колебания активности: полдень -- самое активное время, в полночь наименьшая активность
  • В сравнении с периодом 2019-08-01 -- 2019-08-07 в начальный период 2019-07-25 -- 2019-08-01 практически нет активности
  • Нет предпосылок предполагать такой взрывной рост, скорее всего, это ошибка выгрузки; посмотрим подробнее
In [12]:
plt.figure(figsize=(16, 5))

df.event_dt.hist(bins=150, grid=False);
plt.xlim(datetime(2019, 7, 31), datetime(2019, 8, 8))
plt.title('Распределение количества действий по датам' + "\n", fontsize = 18)
plt.ylabel('Количество действий');

Судя по графику, первый полный день выгрузки -- 2019-08-01, посчитаем процентили:

In [13]:
percentile = pd.DataFrame([[0.5, 0], [1.5, 0]])
percentile.columns = ['Процентиль', 'Дата']
percentile['Дата'] = np.percentile(df.event_date, [0.5, 1.5])
percentile
Out[13]:
Процентиль Дата
0 0.50 2019-07-31
1 1.50 2019-08-01

Гипотеза подтвердилась -- с датой меньше 2019-08-01 только 1.5 % всех данных, фактически выгрузка интервалом в неделю по 2019-08-07

Отбросим данные до 2019-08-01:

In [14]:
df_date_cut = df[df['event_dt'] >= datetime(2019, 8, 1)]

plt.figure(figsize=(16, 5))
df_date_cut.event_dt.hist(bins=100, grid=False);
plt.title('Распределение количества действий по датам' + "\n", fontsize = 18)
plt.ylabel('Количество действий');

Рассчитаем процент отброшенных пользователей и событий:

In [15]:
def losses_count(df_old, df_new):
    print(f'Потеряно событий: {abs(1 - df_old.shape[0] / df_new.shape[0]):0.1%}')
    print(f'Потеряно пользователей: {abs(1 - df_old.device_id_hash.nunique() / df_new.device_id_hash.nunique()):0.1%}')
In [16]:
losses_count(df, df_date_cut)
Потеряно событий: 1.2%
Потеряно пользователей: 0.2%

Проверим распределение пользователей по группам эксперимента:

In [17]:
users_vs_groups = (df_date_cut
                   .groupby('exp_id')['device_id_hash']
                   .nunique()
                   .reset_index())


fig, ax = plt.subplots()

colors = sns.color_palette('deep')
ax.pie(users_vs_groups.device_id_hash, labels=users_vs_groups.exp_id, colors=colors, autopct='%1.0f%%');
ax.set(ylabel=None, title='Распределение количества пользователей по группам');

Количество пользователей в группах практически одинаково

Распределение пользователей по действиям и группам¶

Рассмотрим количество событий и пользователей

In [18]:
print('Выгрузка после отсечения старых дат: \n')
print(f'Всего событий в базе: {df_date_cut.shape[0]}')
print(f'Всего пользователей в базе: {df_date_cut.device_id_hash.nunique()}')
print(f'В среднем {df_date_cut.shape[0] / df_date_cut.device_id_hash.nunique():.1f} события на пользователя')
Выгрузка после отсечения старых дат: 

Всего событий в базе: 240887
Всего пользователей в базе: 7534
В среднем 32.0 события на пользователя
In [19]:
print('Первоначально: \n')
print(f'Всего событий в базе: {df.shape[0]}')
print(f'Всего пользователей в базе: {df.device_id_hash.nunique()}')
print(f'В среднем {df.shape[0] / df.device_id_hash.nunique():.1f} события на пользователя')
Первоначально: 

Всего событий в базе: 243713
Всего пользователей в базе: 7551
В среднем 32.3 события на пользователя
In [20]:
df_date_cut.groupby('device_id_hash')['event_name'].count().describe()
Out[20]:
count   7,534.00
mean       31.97
std        65.09
min         1.00
25%         9.00
50%        19.00
75%        37.00
max     2,307.00
Name: event_name, dtype: float64

При медианном количестве действий на пользователя равном 19, максимальное количество действий составило 2307, визуализируем распределение:

In [21]:
actions_distr = df_date_cut.groupby('device_id_hash').count()

actions_distr.boxplot(column='event_name')
plt.title('Диаграмма размаха количества действий на пользователя \n')
plt.ylabel('Действий за отчетный период')
plt.show()
In [22]:
actions_distr = df_date_cut.groupby('device_id_hash').count()

actions_distr.boxplot(column='event_name')
plt.title('Диаграмма размаха количества действий на пользователя \n')
plt.ylabel('Действий за отчетный период')
plt.ylim(0, 100)
plt.show()

Посчитаем процентили:

In [23]:
percentile = pd.DataFrame([[95, 0], [99, 0]])
percentile.columns = ['Процентиль', 'Количество действий за период']
percentile['Количество действий за период'] = np.percentile(actions_distr.event_name, [95, 99])
percentile
Out[23]:
Процентиль Количество действий за период
0 95 88.00
1 99 201.01
  • 99 % пользователей в выборке в среднем совершают меньше 201 действия за рассматриваемый период
  • В каких группах тестирования находятся эти аномально активные пользователи?
In [24]:
abnormally_active_users = actions_distr.query('event_name >= 201.01').index
abnormal_users_vs_groups = (df_date_cut
                           .query('device_id_hash in @abnormally_active_users')
                           .groupby('exp_id')['device_id_hash']
                           .nunique()
                           .reset_index())


fig, ax = plt.subplots()

ax.pie(abnormal_users_vs_groups.device_id_hash, 
       labels=abnormal_users_vs_groups.exp_id, 
       colors=colors, 
       autopct='%1.0f%%');

ax.set(ylabel=None, title='Распределение количества аномально активных пользователей по группам');
  • Самые активные пользователи распределены по группам условно равномерно
  • Создадим еще один датафрейм, в котором удалим аномально активных пользователей для того, чтобы избежать искажения результатов дальнейшего анализа:
In [25]:
df_actv_date_cut = df_date_cut.query('device_id_hash not in @abnormally_active_users')

Рассчитаем процент потерь по отношению к фрейму с усеченной датой:

In [26]:
losses_count(df_date_cut, df_actv_date_cut)
Потеряно событий: 16.6%
Потеряно пользователей: 1.0%

Рассчитаем процент потерь по отношению к первоначальному фрейму:

In [27]:
losses_count(df, df_actv_date_cut)
Потеряно событий: 18.0%
Потеряно пользователей: 1.2%

Вывод¶

  • Минимальная дата в выгрузке 2019-07-25, однако, по плотности событий сделан вывод, что для анализа пригоден интервал в неделю с 2019-08-01 по 2019-08-07, данные за пределами интервала отброшены; потеряно 1.2 % записей и 0.2 % пользователей
  • Пользователи распределены по тестовым группам равномерно, все три группы представлены
  • Среднее количество действий на пользователя за наблюдаемую неделю 32.3, медиана 19.0, максимальное количество 2307; только 1 % пользователей совершили больше 200 действий, такие аномальные пользователи отброшены; потери составили 16.6 % строк датафрейма корректного временного интервала

Событийная аналитика¶

Рассмотрим пул событий в данных

In [28]:
actions = pd.DataFrame([[0], [1], [2], [3], [4]])
actions.columns = ['actions']
actions['actions'] = df_actv_date_cut.event_name.unique()
actions
Out[28]:
actions
0 Tutorial
1 MainScreenAppear
2 OffersScreenAppear
3 CartScreenAppear
4 PaymentScreenSuccessful

Всего пять событий:

  • Просмотр обучения
  • Просмотр главного экрана
  • Просмотр витрины
  • Просмотр корзины
  • Экран подтверждения платежа

Рассчитаем частоту событий

In [29]:
actions_freq = (df_actv_date_cut
                .groupby('event_name')['device_id_hash']
                .count()
                .to_frame()
                .reset_index())

actions_freq.columns = ['actions', 'total']


fig, ax = plt.subplots()
fig.set(size_inches=(9, 5))
sns.barplot(ax=ax, data=actions_freq.sort_values(by='total', ascending=False), x='total', y='actions', palette='deep');
ax.set(ylabel=None, xlabel='\n Количество совершения действия', title='Распределение событий в выборке по частоте \n');
  • Ожидаемо, самое частое действие -- просмотр главного экрана, с этого начинается путь каждого пользователя
  • Страницу с туториалом просматривают очень редко -- вероятно интерфейс приложения интуитивно понятен пользователям

Рассмотрим, сколько уникальных пользователей хотя бы раз выполнили каждое из событий:

In [30]:
actions_x_users = (df_actv_date_cut
                   .groupby('event_name')['device_id_hash']
                   .nunique()
                   .to_frame()
                   .reset_index())

actions_x_users.columns = ['actions', 'user_count']


fig, ax = plt.subplots()
fig.set(size_inches=(9, 5))
sns.barplot(ax=ax, data=actions_x_users.sort_values(by='user_count', ascending=False), 
            x='user_count', y='actions', palette='deep');
ax.set(ylabel=None, xlabel='\n Количество пользователей', title='Распределение событий по количеству пользователей \n');

Распределение очень похоже на предыдущее, но относительные отклонения от действия к действию отличаются

In [31]:
actions_x_users['fraction_total'] = actions_x_users.user_count / (df_actv_date_cut.device_id_hash.nunique())
actions_x_users.sort_values(by='user_count', ascending=False).reset_index(drop=True)
Out[31]:
actions user_count fraction_total
0 MainScreenAppear 7344 0.98
1 OffersScreenAppear 4517 0.61
2 CartScreenAppear 3658 0.49
3 PaymentScreenSuccessful 3463 0.46
4 Tutorial 824 0.11
  • Почти все пользователи видели главный экран приложения -- 98 %
  • Из 100 % скачавших приложение 46 % совершают покупку
  • Только 11 % пользователей изучали руководство
  • Просмотр руководства не является обязательным действием
  • Полный путь пользователя от открытия приложения до покупки выглядит следующим образом: приложение показывает главный экран, после пользователь должен ознакомиться с предложениями, затем проверить корзину, последним шагом будет оплата заказа, именно такую воронку событий будем анализировать
In [32]:
actions_x_users = actions_x_users.loc[:3,:].sort_values(by='user_count', ascending=False)

fig = go.Figure(go.Funnel(
    y = actions_x_users.actions,
    x = actions_x_users.user_count,
    textposition = "inside",
    textinfo = "value+percent initial"
        )
    ) 
fig.update_layout(
    title="Продуктовая воронка, количество пользователей")
fig.show()
  • Узкое место в пользовательском треке -- это переход от главного экрана к экрану с предложениями: только 62 % пользователей видят витрину приложения;
  • Если пользователь увидел витрину, то дальнейшая конверсия очень хорошая: 81 % видивших витрину, посмотрят, что они добавили в корзину, а 95 % таких пользователей оформят заказ;
  • Необходимо уточнить: зачем в приложении нужен главный экран? Возможно ли отказаться от него? Если на этом этапе мы собираем в том числе критическую информацию (город и адрес, а значит возможность доставки и доступный ассортимент), то вероятно стоит собирать только ее, а оставшуюся часть отложить на время после просмотра витрины.

Вывод¶

  • В приложении всего пять событий:
    • Просмотр обучения
    • Просмотр главного экрана
    • Просмотр витрины
    • Просмотр корзины
    • Экран подтверждения платежа
  • Руководство просматривают совсем редко: только 11 % пользователей обращались к его странице
  • Из всех скачавших приложение пользователей, только 2 % пользователей не открывают приложение
  • Из всех скачавших приложение пользователей, 46 % совершают покупку
  • Пользовательский трек выглядит следующим образом: главный экран -> витрина -> корзина -> оплата
  • От витрины к корзине переходят 81 % пользователей, от корзины к оплате переходят 95 %
  • Узкое место в треке -- переход с главного экрана к витрине: здесь мы теряем 38 % пользователей, всего 62 % пользователей идут дальше
  • Рекомендуется отказаться от главного экрана, если это возможно -- сразу показывать витрину; если это невозможно, то необходимо проанализировать: что на главном экране отталкивает значительное количество пользователей?

Анализ результатов эксперимента¶

Проверим корректность отнесения пользователя к группе: есть ли такие пользователи, у которых группа меняется в ходе тестирования?

In [33]:
incorrect_visitors = (df_actv_date_cut[['device_id_hash', 'exp_id']]
                      .drop_duplicates()) # collecting unique couples: user / group number
incorrect_visitors.device_id_hash.duplicated().sum() # are there duplicate users among these unique couples?
Out[33]:
0

Все пользователи находятся в одной группе на протяжении всего теста

  • Сначала проверим: есть ли статистически значимое отклонение среднего количества действий у двух контрольных групп (А/А-тест)
  • Выделим записи контрольных пользователей в отдельные датафреймы, сгруппируем по пользователю и посчитаем количество записей на пользователя:
In [34]:
sample_A1 = df_actv_date_cut.query('exp_id == 246').groupby('device_id_hash')['event_name'].count()
sample_A2 = df_actv_date_cut.query('exp_id == 247').groupby('device_id_hash')['event_name'].count()

display(sample_A1.head())
sample_A2.head()
device_id_hash
6888746892508752       1
6922444491712477      47
8740973466195562       9
12692216027168046     10
15708180189885246    126
Name: event_name, dtype: int64
Out[34]:
device_id_hash
6909561520679493       5
7702139951469979     137
28534696657485531     29
28755862496905658      8
29094035245869447     24
Name: event_name, dtype: int64

H0: Среднее количество действий пользователя группы 246 относительно группы 247 одинаковое

H1: Среднее количество действий пользователя группы 246 относительно группы 247 отличается

Принимаем уровень статистической значимости 5 %

In [35]:
print("p-value составляет", "{0:.2%}".format(st.mannwhitneyu(sample_A1, sample_A2)[1]))

print("Относительное отклонение среднего количества действий пользователя группы 246 относительно группы 247: " "{0:.1%}".format(sample_A1.mean()
                       /sample_A2.mean()-1))
p-value составляет 99.45%
Относительное отклонение среднего количества действий пользователя группы 246 относительно группы 247: 0.1%
  • Вероятность ошибиться при отказе от нулевой гипотезы составила 99.45 %, это значительно больше принятого уровня статистической значимости -- не удалось отвергнуть нулевую гипотезу

  • Статистически значимого различия в среднем количестве действий на пользователя между группами не обнаружено

  • Проведем статистические тесты для сравнения долей пользователей в группах
  • Определим для каждого доступного действия доли пользователей, совершивших это действие, и сравним эти доли для контрольных и тестовой группы
In [36]:
def sample_comparison(s1, s2, event, alpha=0.05): # z-test for proportions
    
    # calculating target/total number of users for group №1 on input
    sample1 = df_actv_date_cut.query('exp_id in @s1') 
    all_users_cnt_1 = (sample1
                       .device_id_hash
                       .nunique())
    target_users_cnt_1 = (sample1
                          .query('event_name == @event')
                          .device_id_hash
                          .nunique())
    
    # calculating target/total number of users for group №2 on input
    sample2 = df_actv_date_cut.query('exp_id in @s2')
    all_users_cnt_2 = (sample2
                       .device_id_hash
                       .nunique())
    target_users_cnt_2 = (sample2
                          .query('event_name == @event')
                          .device_id_hash
                          .nunique())
    
    p_val = (proportions_ztest(
                               [target_users_cnt_1, target_users_cnt_2], 
                               [all_users_cnt_1, all_users_cnt_2], 
                               alternative="two-sided")[1]
                              )
    
    print('------------------------')
    print(f'Событие {event} \n')
    print(f'Группа {s1} -- пользователи вызывали: {target_users_cnt_1} из {all_users_cnt_1} ({(target_users_cnt_1 / all_users_cnt_1):.1%})')
    print(f'Группа {s2} -- пользователи вызывали: {target_users_cnt_2} из {all_users_cnt_2} ({(target_users_cnt_2 / all_users_cnt_2):.1%})')
    print('')
    print(f'H0: Доли пользователей, совершивших действие {event} групп {s1} и {s2} одинаковые')
    print(f'H1: Доли пользователей, совершивших действие {event} групп {s1} и {s2} отличаются')
    print('')
    print(f'p-value: {p_val:.2%}, alpha: {alpha:.2%}')
    if p_val < alpha:
        print('+ отвергаем нулевую гипотезу в пользу альтернативной')
    else: print('- не удалось отвергнуть нулевую гипотезу')
    print('')
In [37]:
# comparing groups: 246 vs 247 / 246 vs 248 / 247 vs 248 / 246+247 vs 248

first_groups = [[246], [246], [247], [246, 247]]
second_groups = [[247], [248], [248], [248]]
list_of_events = df_actv_date_cut.event_name.unique()
  • Мы проводим множество проверок: нужно сделать поправку для соблюдения совокупного уровня статистической значимости в 5 %
  • Применим метод Шидака
In [38]:
alpha_Šidák = 1 - (1 - 0.05) ** (1 / 20)

for i in range(len(first_groups)):
    for evnt in list_of_events:
        sample_comparison(first_groups[i], second_groups[i], evnt, alpha_Šidák)
------------------------
Событие Tutorial 

Группа [246] -- пользователи вызывали: 269 из 2456 (11.0%)
Группа [247] -- пользователи вызывали: 279 из 2491 (11.2%)

H0: Доли пользователей, совершивших действие Tutorial групп [246] и [247] одинаковые
H1: Доли пользователей, совершивших действие Tutorial групп [246] и [247] отличаются

p-value: 78.15%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие MainScreenAppear 

Группа [246] -- пользователи вызывали: 2423 из 2456 (98.7%)
Группа [247] -- пользователи вызывали: 2454 из 2491 (98.5%)

H0: Доли пользователей, совершивших действие MainScreenAppear групп [246] и [247] одинаковые
H1: Доли пользователей, совершивших действие MainScreenAppear групп [246] и [247] отличаются

p-value: 67.31%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие OffersScreenAppear 

Группа [246] -- пользователи вызывали: 1514 из 2456 (61.6%)
Группа [247] -- пользователи вызывали: 1498 из 2491 (60.1%)

H0: Доли пользователей, совершивших действие OffersScreenAppear групп [246] и [247] одинаковые
H1: Доли пользователей, совершивших действие OffersScreenAppear групп [246] и [247] отличаются

p-value: 27.70%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие CartScreenAppear 

Группа [246] -- пользователи вызывали: 1238 из 2456 (50.4%)
Группа [247] -- пользователи вызывали: 1216 из 2491 (48.8%)

H0: Доли пользователей, совершивших действие CartScreenAppear групп [246] и [247] одинаковые
H1: Доли пользователей, совершивших действие CartScreenAppear групп [246] и [247] отличаются

p-value: 26.30%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие PaymentScreenSuccessful 

Группа [246] -- пользователи вызывали: 1172 из 2456 (47.7%)
Группа [247] -- пользователи вызывали: 1136 из 2491 (45.6%)

H0: Доли пользователей, совершивших действие PaymentScreenSuccessful групп [246] и [247] одинаковые
H1: Доли пользователей, совершивших действие PaymentScreenSuccessful групп [246] и [247] отличаются

p-value: 13.59%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие Tutorial 

Группа [246] -- пользователи вызывали: 269 из 2456 (11.0%)
Группа [248] -- пользователи вызывали: 276 из 2511 (11.0%)

H0: Доли пользователей, совершивших действие Tutorial групп [246] и [248] одинаковые
H1: Доли пользователей, совершивших действие Tutorial групп [246] и [248] отличаются

p-value: 96.50%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие MainScreenAppear 

Группа [246] -- пользователи вызывали: 2423 из 2456 (98.7%)
Группа [248] -- пользователи вызывали: 2467 из 2511 (98.2%)

H0: Доли пользователей, совершивших действие MainScreenAppear групп [246] и [248] одинаковые
H1: Доли пользователей, совершивших действие MainScreenAppear групп [246] и [248] отличаются

p-value: 24.38%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие OffersScreenAppear 

Группа [246] -- пользователи вызывали: 1514 из 2456 (61.6%)
Группа [248] -- пользователи вызывали: 1505 из 2511 (59.9%)

H0: Доли пользователей, совершивших действие OffersScreenAppear групп [246] и [248] одинаковые
H1: Доли пользователей, совершивших действие OffersScreenAppear групп [246] и [248] отличаются

p-value: 21.75%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие CartScreenAppear 

Группа [246] -- пользователи вызывали: 1238 из 2456 (50.4%)
Группа [248] -- пользователи вызывали: 1204 из 2511 (47.9%)

H0: Доли пользователей, совершивших действие CartScreenAppear групп [246] и [248] одинаковые
H1: Доли пользователей, совершивших действие CartScreenAppear групп [246] и [248] отличаются

p-value: 8.32%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие PaymentScreenSuccessful 

Группа [246] -- пользователи вызывали: 1172 из 2456 (47.7%)
Группа [248] -- пользователи вызывали: 1155 из 2511 (46.0%)

H0: Доли пользователей, совершивших действие PaymentScreenSuccessful групп [246] и [248] одинаковые
H1: Доли пользователей, совершивших действие PaymentScreenSuccessful групп [246] и [248] отличаются

p-value: 22.39%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие Tutorial 

Группа [247] -- пользователи вызывали: 279 из 2491 (11.2%)
Группа [248] -- пользователи вызывали: 276 из 2511 (11.0%)

H0: Доли пользователей, совершивших действие Tutorial групп [247] и [248] одинаковые
H1: Доли пользователей, совершивших действие Tutorial групп [247] и [248] отличаются

p-value: 81.42%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие MainScreenAppear 

Группа [247] -- пользователи вызывали: 2454 из 2491 (98.5%)
Группа [248] -- пользователи вызывали: 2467 из 2511 (98.2%)

H0: Доли пользователей, совершивших действие MainScreenAppear групп [247] и [248] одинаковые
H1: Доли пользователей, совершивших действие MainScreenAppear групп [247] и [248] отличаются

p-value: 45.45%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие OffersScreenAppear 

Группа [247] -- пользователи вызывали: 1498 из 2491 (60.1%)
Группа [248] -- пользователи вызывали: 1505 из 2511 (59.9%)

H0: Доли пользователей, совершивших действие OffersScreenAppear групп [247] и [248] одинаковые
H1: Доли пользователей, совершивших действие OffersScreenAppear групп [247] и [248] отличаются

p-value: 88.51%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие CartScreenAppear 

Группа [247] -- пользователи вызывали: 1216 из 2491 (48.8%)
Группа [248] -- пользователи вызывали: 1204 из 2511 (47.9%)

H0: Доли пользователей, совершивших действие CartScreenAppear групп [247] и [248] одинаковые
H1: Доли пользователей, совершивших действие CartScreenAppear групп [247] и [248] отличаются

p-value: 53.97%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие PaymentScreenSuccessful 

Группа [247] -- пользователи вызывали: 1136 из 2491 (45.6%)
Группа [248] -- пользователи вызывали: 1155 из 2511 (46.0%)

H0: Доли пользователей, совершивших действие PaymentScreenSuccessful групп [247] и [248] одинаковые
H1: Доли пользователей, совершивших действие PaymentScreenSuccessful групп [247] и [248] отличаются

p-value: 78.01%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие Tutorial 

Группа [246, 247] -- пользователи вызывали: 548 из 4947 (11.1%)
Группа [248] -- пользователи вызывали: 276 из 2511 (11.0%)

H0: Доли пользователей, совершивших действие Tutorial групп [246, 247] и [248] одинаковые
H1: Доли пользователей, совершивших действие Tutorial групп [246, 247] и [248] отличаются

p-value: 91.11%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие MainScreenAppear 

Группа [246, 247] -- пользователи вызывали: 4877 из 4947 (98.6%)
Группа [248] -- пользователи вызывали: 2467 из 2511 (98.2%)

H0: Доли пользователей, совершивших действие MainScreenAppear групп [246, 247] и [248] одинаковые
H1: Доли пользователей, совершивших действие MainScreenAppear групп [246, 247] и [248] отличаются

p-value: 26.19%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие OffersScreenAppear 

Группа [246, 247] -- пользователи вызывали: 3012 из 4947 (60.9%)
Группа [248] -- пользователи вызывали: 1505 из 2511 (59.9%)

H0: Доли пользователей, совершивших действие OffersScreenAppear групп [246, 247] и [248] одинаковые
H1: Доли пользователей, совершивших действие OffersScreenAppear групп [246, 247] и [248] отличаются

p-value: 42.80%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие CartScreenAppear 

Группа [246, 247] -- пользователи вызывали: 2454 из 4947 (49.6%)
Группа [248] -- пользователи вызывали: 1204 из 2511 (47.9%)

H0: Доли пользователей, совершивших действие CartScreenAppear групп [246, 247] и [248] одинаковые
H1: Доли пользователей, совершивших действие CartScreenAppear групп [246, 247] и [248] отличаются

p-value: 17.62%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

------------------------
Событие PaymentScreenSuccessful 

Группа [246, 247] -- пользователи вызывали: 2308 из 4947 (46.7%)
Группа [248] -- пользователи вызывали: 1155 из 2511 (46.0%)

H0: Доли пользователей, совершивших действие PaymentScreenSuccessful групп [246, 247] и [248] одинаковые
H1: Доли пользователей, совершивших действие PaymentScreenSuccessful групп [246, 247] и [248] отличаются

p-value: 59.09%, alpha: 0.26%
- не удалось отвергнуть нулевую гипотезу

  • Для двух контрольных групп не удалось отвергнуть ни одну гипотезу об одинаковых долях пользователей, совершивших любое действие -- делаем вывод о корректном разбиении на группы
  • При сравнении контрольных групп с тестовой по отдельности, а также комбинируя контрольные группы, не удалось отвергнуть ни одну гипотезу об одинаковых долях пользователей, совершивших любое действие
  • Не удалось зафиксировать статистически значимого отличия между тестом и контролем

Вывод¶

  • Пользователи равномерно распределены между тремя группами тестирования
  • Отнесение пользователя к группе тестирования не меняется в ходе эксперимента
  • Для двух контрольных групп не удалось отвергнуть ни одну гипотезу об одинаковом количестве любых действий -- делаем вывод о корректном разбиении на группы
  • Не удалось зафиксировать статистически значимого отличия между группой с измененными шрифтами и контрольной

Общий вывод¶

Предобработка данных

  • Для удобства работы изменены названия столбцов
  • Пропусков в данных не обнаружено
  • Удалены полные дубликаты, удаленные данные составили 0.2 % всех строк
  • Добавлено два столбца с датой в формате datetime

Исследовательский анализ данных

  • Минимальная дата в выгрузке 2019-07-25, однако, по плотности событий сделан вывод, что для анализа пригоден интервал в неделю с 2019-08-01 по 2019-08-07, данные за пределами интервала отброшены; потеряно 1.2 % записей и 0.2 % пользователей
  • Пользователи распределены по тестовым группам равномерно, все три группы представлены
  • Среднее количество действий на пользователя за наблюдаемую неделю 32.3, медиана 19.0, максимальное количество 2307; только 1 % пользователей совершили больше 200 действий, такие аномальные пользователи отброшены; потери составили 16.6 % строк датафрейма корректного временного интервала

Событийная аналитика

  • В приложении всего пять событий:
    • Просмотр обучения
    • Просмотр главного экрана
    • Просмотр витрины
    • Просмотр корзины
    • Экран подтверждения платежа
  • Руководство просматривают совсем редко: только 11 % пользователей обращались к его странице
  • Из всех скачавших приложение пользователей, только 2 % пользователей не открывают приложение
  • Из всех скачавших приложение пользователей, 46 % совершают покупку
  • Пользовательский трек выглядит следующим образом: главный экран -> витрина -> корзина -> оплата
  • От витрины к корзине переходят 81 % пользователей, от корзины к оплате переходят 95 %
  • Узкое место в треке -- переход с главного экрана к витрине: здесь мы теряем 38 % пользователей, всего 62 % пользователей идут дальше
  • Рекомендуется отказаться от главного экрана, если это возможно -- сразу показывать витрину; если это невозможно, то необходимо проанализировать: что на главном экране отталкивает значительное количество пользователей?

Анализ результатов эксперимента

  • Пользователи равномерно распределены между тремя группами тестирования
  • Отнесение пользователя к группе тестирования не меняется в ходе эксперимента
  • Для двух контрольных групп не удалось отвергнуть ни одну гипотезу об одинаковом количестве любых действий -- делаем вывод о корректном разбиении на группы
  • Не удалось зафиксировать статистически значимого отличия между группой с измененными шрифтами и контрольной

Рекомендации

  • Не рекомендуется использовать новые шрифты -- статистически значимого эффекта на конверсию не обнаружено